<?php
/**
 * Created by PhpStorm.
 * User: Appla
 * Date: 2017/5/16
 * Time: 11:42
 */

namespace test;

require __DIR__.'/CurlRequestLogger.php';

use Redis;
use DateTime;
use DateInterval;
use artisan\cache;
use test\CurlRequestLogger;

/**
 * 统计Curl请求的情况记录HTTP状态和Curl请求状态
 * HTTP status codes:
 * @see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 * @see https://curl.haxx.se/libcurl/c/libcurl-errors.html
 * curl error code最大没有超过100,所以直接和HTTP状态码混用了
 * Class AnalysisCurlRequestLog
 * @package test
 */
class AnalysisCurlRequestLog
{
    const CACHE_KEY_TPL = CurlRequestLogger::CACHE_KEY_TPL;

    const CACHE_KEY_PREFIX = CurlRequestLogger::CACHE_KEY_PREFIX;

    const REDIS_CONFIG = CurlRequestLogger::REDIS_CONFIG;

    const TIME_FORMAT = CurlRequestLogger::TIME_FORMAT;

    const CACHE_INSTANCE_ID = CurlRequestLogger::CACHE_INSTANCE_ID;

    /**
     * @var \artisan\cache\PHPredisDriver|\Redis
     */
    private static $cacheInstance;

    private $url;
    private $rawUrl;
    private $timeLength;
    private $cacheKeys = [];
    private $lastQueryResult;
    private $keyPatterns = [];
    private $aggregatedData = [];
    private $timeInfo = [];

    /**
     * @return array
     */
    public function handle()
    {
        return $this->getRequestInfo(
            isset($_REQUEST['url']) ? urldecode($_REQUEST['url']) : null,
            isset($_REQUEST['minutes']) ? $_REQUEST['minutes'] : 30
            );
    }

    /**
     * @param string|null $url
     * @param int $minutes
     */
    public function getRequestInfo($url = null, $minutes = 30)
    {
        $url && $this->url = CurlRequestLogger::parseUrl($this->rawUrl = $url);
        $this->timeLength = $minutes;
        return $this->analysis();
    }

    /**
     * @return array
     */
    private function analysis()
    {
        $this->getCacheKeys();
        $this->getAllData();
        $data =  [
            'data' => $this->summarizeData(),
            'timeInfo' => $this->timeInfo,
        ];
        $this->printOut($data);
        return $data;
    }

    /**
     * @param  array $data
     * @param  array $conditions
     * @return array
     */
    private function sort(array $data, array $conditions)
    {
        $params = $this->parseSortOrder($conditions, $data);
        return $params ? array_multisort($data, ...$params) : $data;
    }

    /**
     * parseSortOrder
     *
     * @param  array  $conditions
     * @param  array $data
     * @return array
     */
    private function parseSortOrder(array $conditions, array $data)
    {
        $params = [];
        $testData = reset($data);
        $orderMap = ['asc', 'desc', 'ASC', 'DESC'];
        foreach ($conditions as $key => $order) {
            if (!in_array($order, $orderMap, true) || !isset($testData[$key])) {
                continue;
            }
            $params[] = array_column($data, $key);
            $params[] = strtolower($order) === 'asc' ? SORT_ASC : SORT_DESC;
        }
        return $params;
    }

    /**
     * @param array $result
     */
    private function printOut(array $result)
    {
        list($data, $timeInfo) = array_values($result);
        if($data){
            printf('为您展示最近%d分钟的Curl请求数据![%s-%s]' . PHP_EOL, $timeInfo['timeLength'], $timeInfo['startTime'], $timeInfo['endTime']);
            foreach ($data as $url => $datum) {
                printf('url: %s' . PHP_EOL, $url);
                foreach ($datum as $code => $count){
                    $codeInfo = self::getErrorInfo($code);
                    printf('type:%s, code: %s,info: %s, count: %s' . PHP_EOL, $codeInfo['type'], $code, $codeInfo['info'], $count);
                }
                echo '---------------------------------------------------', PHP_EOL;
            }
        }else{
            echo '暂无数据!', PHP_EOL;
        }
    }

    /**
     * @return array
     */
    private function getCacheKeys()
    {
        $allCacheKeys = [];
        if ($this->timeLength) {
            $timeStrArr = [];
            $timeObj = new DateTime();
            $this->timeInfo['endTime'] = $timeObj->format('Y-m-d H:i:s');
            $i = 0;
            while ($i++ < $this->timeLength) {
                $timeStrArr[] = $timeObj->format(self::TIME_FORMAT);
                $timeObj = $timeObj->sub(new DateInterval('PT1M'));
            }
            $this->timeInfo['startTime'] = $timeObj->format('Y-m-d H:i:s');
            $this->timeInfo['timeLength'] = $this->timeLength;
            if ($this->url) {
                $allCacheKeys = array_map(function ($timeStr) {
                    return sprintf(self::CACHE_KEY_TPL, $timeStr, $this->url);
                }, $timeStrArr);
            } else {
                $allCacheKeys = $this->getCacheKeyByPatterns($this->summarizeTimeStrRange($timeStrArr));
            }
        }
        return $this->cacheKeys = $allCacheKeys;
    }

    /**
     * @return array
     */
    private function getAllData()
    {
        $data = [];
        if ($this->cacheKeys) {
            foreach ($this->cacheKeys as $cacheKey) {
                $data[$cacheKey] = self::getCacheInstance()->hGetAll($cacheKey);
            }
        }
        return $this->lastQueryResult = array_filter($data);
    }

    /**
     * @return array
     */
    private function summarizeData()
    {
        $data = [];
        foreach ($this->lastQueryResult as $key => $logs) {
            $url = $this->url ?: ltrim(strrchr($key, ':'), ':');
            if(isset($data[$url])){
                $tmpData = &$data[$url];
                foreach ($logs as $code => $value){
                    isset($tmpData[$code]) && ($tmpData[$code] += $value) || ($tmpData[$code] = (int)$value);
                }
            }else{
                $data[$url] = $logs;
            }
        }
        return $this->aggregatedData = $data;
    }

    /**
     * @param array $timeStrArr
     * @return array
     */
    private function summarizeTimeStrRange(array $timeStrArr)
    {
        $aggregatedTimeStrArr = [];
        foreach ($timeStrArr as $item) {
            $aggregatedTimeStrArr[(string)substr($item, 0, -1)][] = $item;
        }
        $allKeyPatterns = [];
        foreach ($aggregatedTimeStrArr as $key => $item) {
            if (count($item) === 10) {
                $allKeyPatterns[] = $key;
            } else {
                $allKeyPatterns = array_merge($allKeyPatterns, $item);
            }
        }
        return $this->keyPatterns = $allKeyPatterns;
    }

    /**
     * @param array $keyPatterns
     * @return array
     */
    private function getCacheKeyByPatterns(array $keyPatterns)
    {
        $cacheKeyPatternTpl = self::CACHE_KEY_PREFIX.'%s%s';
        $allMatchedCacheKeys = array_filter(array_map(function ($timeStr) use($cacheKeyPatternTpl){
            return self::getCacheInstance()->keys(sprintf($cacheKeyPatternTpl, $timeStr, '*'));
        }, $keyPatterns));
        return $allMatchedCacheKeys ? array_merge(...$allMatchedCacheKeys) : [];
    }

    /**
     * @return \artisan\cache\PHPredisDriver|\Redis
     */
    private static function getCacheInstance()
    {
        return self::$cacheInstance ?: self::$cacheInstance = (function () {
            $redis = new Redis();
            $redis->connect(...self::REDIS_CONFIG);
            return $redis;
        })();
//        return self::$cacheInstance ?: self::$cacheInstance = cache::connect(self::CACHE_INSTANCE_ID);
    }

    /**
     * @param $code
     * @return array
     */
    private static function getErrorInfo($code)
    {
        $info = [
          'code' => $code,
          'type' => 'NotFound',
          'info' => '',
        ];
        if($code){
            if($code > 99 ){
                $info['type'] = 'HTTP_Status_Code';
                $info['info'] = self::getHttpStatusInfoByCode($code);
            }else{
                $info['info'] = self::getCurlErrorByCode($code);
                $info['type'] = 'cUrl_Error_Code';
            }
        }
        return $info;
    }

    /**
     * @see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
     * @param int $code
     * @return string
     */
    private static function getHttpStatusInfoByCode($code)
    {
        static $httpStatusCodeMap = [
            100 => 'Continue',
            101 => 'Switching Protocols',
            102 => 'Processing (WebDAV; RFC 2518)',
            200 => 'OK',
            201 => 'Created',
            202 => 'Accepted',
            203 => 'Non-Authoritative Information (since HTTP/1.1)',
            204 => 'No Content',
            205 => 'Reset Content',
            206 => 'Partial Content (RFC 7233)',
            207 => 'Multi-Status (WebDAV; RFC 4918)',
            208 => 'Already Reported (WebDAV; RFC 5842)',
            226 => 'IM Used (RFC 3229)',
            300 => 'Multiple Choices',
            301 => 'Moved Permanently',
            302 => 'Found',
            303 => 'See Other (since HTTP/1.1)',
            304 => 'Not Modified (RFC 7232)',
            305 => 'Use Proxy (since HTTP/1.1)',
            306 => 'Switch Proxy',
            307 => 'Temporary Redirect (since HTTP/1.1)',
            308 => 'Permanent Redirect (RFC 7538)',
            400 => 'Bad Request',
            401 => 'Unauthorized (RFC 7235)',
            402 => 'Payment Required',
            403 => 'Forbidden',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            406 => 'Not Acceptable',
            407 => 'Proxy Authentication Required (RFC 7235)',
            408 => 'Request Timeout',
            409 => 'Conflict',
            410 => 'Gone',
            411 => 'Length Required',
            412 => 'Precondition Failed (RFC 7232)',
            413 => 'Payload Too Large (RFC 7231)',
            414 => 'URI Too Long (RFC 7231)',
            415 => 'Unsupported Media Type',
            416 => 'Range Not Satisfiable (RFC 7233)',
            417 => 'Expectation Failed',
            418 => 'I\'m a teapot (RFC 2324)',
            421 => 'Misdirected Request (RFC 7540)',
            422 => 'UnProcessable Entity (WebDAV; RFC 4918)',
            423 => 'Locked (WebDAV; RFC 4918)',
            424 => 'Failed Dependency (WebDAV; RFC 4918)',
            426 => 'Upgrade Required',
            428 => 'Precondition Required (RFC 6585)',
            429 => 'Too Many Requests (RFC 6585)',
            431 => 'Request Header Fields Too Large (RFC 6585)',
            451 => 'Unavailable For Legal Reasons (RFC 7725)',
            500 => 'Internal Server Error',
            501 => 'Not Implemented',
            502 => 'Bad Gateway',
            503 => 'Service Unavailable',
            504 => 'Gateway Timeout',
            505 => 'HTTP Version Not Supported',
            506 => 'Variant Also Negotiates (RFC 2295)',
            507 => 'Insufficient Storage (WebDAV; RFC 4918)',
            508 => 'Loop Detected (WebDAV; RFC 5842)',
            510 => 'Not Extended (RFC 2774)',
            511 => 'Network Authentication Required (RFC 6585)',
            //Unofficial codes
            103 => 'Checkpoint',
//            103 => 'Early Hints',
            420 => 'Method Failure (Spring Framework)',
//            420 => 'Enhance Your Calm (Twitter)',
            450 => 'Blocked by Windows Parental Controls (Microsoft)',
            498 => 'Invalid Token (Esri)',
            499 => 'Token Required (Esri)',
            509 => 'Bandwidth Limit Exceeded (Apache Web Server/cPanel)',
            530 => 'Site is frozen',
            598 => '(Informal convention) Network read timeout error',
            599 => '(Informal convention) Network connect timeout error',
            //Internet Information Services
            440 => 'Login Time-out',
            449 => 'Retry With',
//            451 => 'Redirect',
            //nginx
            444 => 'No Response',
            495 => 'SSL Certificate Error',
            496 => 'SSL Certificate Required',
            497 => 'HTTP Request Sent to HTTPS Port',
//            499 => 'Client Closed Request',
            //Cloudflare
            520 => 'Unknown Error',
            521 => 'Web Server Is Down',
            522 => 'Connection Timed Out',
            523 => 'Origin Is Unreachable',
            524 => 'A Timeout Occurred',
            525 => 'SSL Handshake Failed',
            526 => 'Invalid SSL Certificate',
            527 => 'Railgun Error',
        ];
        return isset($httpStatusCodeMap[$code]) ? $httpStatusCodeMap[$code] : '';
    }

    /**
     * @see https://curl.haxx.se/libcurl/c/libcurl-errors.html
     * @param int $code
     * @return string
     */
    private static function getCurlErrorByCode($code)
    {
        static $curlErrorCodeMap = [
            0 => 'CURLE_OK',
            1 => 'CURLE_UNSUPPORTED_PROTOCOL',
            2 => 'CURLE_FAILED_INIT',
            3 => 'CURLE_URL_MALFORMAT',
            4 => 'CURLE_NOT_BUILT_IN',
            5 => 'CURLE_COULDNT_RESOLVE_PROXY',
            6 => 'CURLE_COULDNT_RESOLVE_HOST',
            7 => 'CURLE_COULDNT_CONNECT',
            8 => 'CURLE_FTP_WEIRD_SERVER_REPLY',
            9 => 'CURLE_REMOTE_ACCESS_DENIED',
            10 => 'CURLE_FTP_ACCEPT_FAILED',
            11 => 'CURLE_FTP_WEIRD_PASS_REPLY',
            12 => 'CURLE_FTP_ACCEPT_TIMEOUT',
            13 => 'CURLE_FTP_WEIRD_PASV_REPLY',
            14 => 'CURLE_FTP_WEIRD_227_FORMAT',
            15 => 'CURLE_FTP_CANT_GET_HOST',
            16 => 'CURLE_HTTP2',
            17 => 'CURLE_FTP_COULDNT_SET_TYPE',
            18 => 'CURLE_PARTIAL_FILE',
            19 => 'CURLE_FTP_COULDNT_RETR_FILE',
            21 => 'CURLE_QUOTE_ERROR',
            22 => 'CURLE_HTTP_RETURNED_ERROR',
            23 => 'CURLE_WRITE_ERROR',
            25 => 'CURLE_UPLOAD_FAILED',
            26 => 'CURLE_READ_ERROR',
            27 => 'CURLE_OUT_OF_MEMORY',
            28 => 'CURLE_OPERATION_TIMEDOUT',
            30 => 'CURLE_FTP_PORT_FAILED',
            31 => 'CURLE_FTP_COULDNT_USE_REST',
            33 => 'CURLE_RANGE_ERROR',
            34 => 'CURLE_HTTP_POST_ERROR',
            35 => 'CURLE_SSL_CONNECT_ERROR',
            36 => 'CURLE_BAD_DOWNLOAD_RESUME',
            37 => 'CURLE_FILE_COULDNT_READ_FILE',
            38 => 'CURLE_LDAP_CANNOT_BIND',
            39 => 'CURLE_LDAP_SEARCH_FAILED',
            41 => 'CURLE_FUNCTION_NOT_FOUND',
            42 => 'CURLE_ABORTED_BY_CALLBACK',
            43 => 'CURLE_BAD_FUNCTION_ARGUMENT',
            45 => 'CURLE_INTERFACE_FAILED',
            47 => 'CURLE_TOO_MANY_REDIRECTS',
            48 => 'CURLE_UNKNOWN_OPTION',
            49 => 'CURLE_TELNET_OPTION_SYNTAX',
            51 => 'CURLE_PEER_FAILED_VERIFICATION',
            52 => 'CURLE_GOT_NOTHING',
            53 => 'CURLE_SSL_ENGINE_NOTFOUND',
            54 => 'CURLE_SSL_ENGINE_SETFAILED',
            55 => 'CURLE_SEND_ERROR',
            56 => 'CURLE_RECV_ERROR',
            58 => 'CURLE_SSL_CERTPROBLEM',
            59 => 'CURLE_SSL_CIPHER',
            60 => 'CURLE_SSL_CACERT',
            61 => 'CURLE_BAD_CONTENT_ENCODING',
            62 => 'CURLE_LDAP_INVALID_URL',
            63 => 'CURLE_FILESIZE_EXCEEDED',
            64 => 'CURLE_USE_SSL_FAILED',
            65 => 'CURLE_SEND_FAIL_REWIND',
            66 => 'CURLE_SSL_ENGINE_INITFAILED',
            67 => 'CURLE_LOGIN_DENIED',
            68 => 'CURLE_TFTP_NOTFOUND',
            69 => 'CURLE_TFTP_PERM',
            70 => 'CURLE_REMOTE_DISK_FULL',
            71 => 'CURLE_TFTP_ILLEGAL',
            72 => 'CURLE_TFTP_UNKNOWNID',
            73 => 'CURLE_REMOTE_FILE_EXISTS',
            74 => 'CURLE_TFTP_NOSUCHUSER',
            75 => 'CURLE_CONV_FAILED',
            76 => 'CURLE_CONV_REQD',
            77 => 'CURLE_SSL_CACERT_BADFILE',
            78 => 'CURLE_REMOTE_FILE_NOT_FOUND',
            79 => 'CURLE_SSH',
            80 => 'CURLE_SSL_SHUTDOWN_FAILED',
            81 => 'CURLE_AGAIN',
            82 => 'CURLE_SSL_CRL_BADFILE',
            83 => 'CURLE_SSL_ISSUER_ERROR',
            84 => 'CURLE_FTP_PRET_FAILED',
            85 => 'CURLE_RTSP_CSEQ_ERROR',
            86 => 'CURLE_RTSP_SESSION_ERROR',
            87 => 'CURLE_FTP_BAD_FILE_LIST',
            88 => 'CURLE_CHUNK_FAILED',
            89 => 'CURLE_NO_CONNECTION_AVAILABLE',
            90 => 'CURLE_SSL_PINNEDPUBKEYNOTMATCH',
            91 => 'CURLE_SSL_INVALIDCERTSTATUS',
            92 => 'CURLE_HTTP2_STREAM',
        ];
        return isset($curlErrorCodeMap[$code]) ? $curlErrorCodeMap[$code]: 'CURLE_OBSOLETE*';
    }
}

(new AnalysisCurlRequestLog())->getRequestInfo();
